/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package HTTPDownloader;

import GUI.FormatOutput;
import GUI.MainWindow;
import Tools.AppDataManager;
import Tools.FileTools;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.Timer;
import java.util.TimerTask;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.JTable;
import javax.swing.table.DefaultTableModel;

/**
 *
 * @author patricksamson236
 */
public class HTTPTaskManager extends Thread {

    // connections
    private URL source;
    //task properties
    private long timeCreated;
    private long timeCompleted;
    private long timeWorking;
    private long lastTimeUpdate;
    // task info
    private long contentLength;
    private String taskName;
    private Status taskStatus;
    private JTable table;
    private MainWindow mainWindow;
    // workers
    private int numActiveWorkers;
    private int numWorkers;
    private DownloadWorker[] arWorkers;
    private int maxBufferSize;
    private int lastWorkerID;
    // task progress
    private Timer timer_main = new Timer();
    private Timer timer_temp = new Timer();
    private TimerTask timerTask;
    private double lastInstSpeed;
    private double lastAvgSpeed;
    private long lastDownloadedSession;
    private long lastDownloadedTotal;
    // HDD IO
    private FileTools fileTools = new FileTools();
    private File destination;
    private RandomAccessFile raf;
    //tools
    private RangeManager RangeManager = new RangeManager();
    private FormatOutput format = new FormatOutput();

    public HTTPTaskManager(int numWorkers, int maxBufferSize, File destination) throws IOException {
        this.numWorkers = numWorkers;
        this.maxBufferSize = maxBufferSize;
        this.destination = destination;
        this.lastWorkerID = 1;
        this.taskStatus = Status.WAITING;
        this.timeWorking = 0;
        this.timeCreated = System.currentTimeMillis();
        this.timeCompleted = this.timeCreated;
        this.RangeManager.setContentLength(contentLength);
        this.RangeManager.setNumWorkers(numWorkers);
    }

    public void resume(long timeCreated, long timeCompleted, long timeWorking,
            String taskName, Status taskStatus, long contentLength, Object[] RangeProgression) {
        this.timeCreated = timeCreated;
        this.timeCompleted = timeCompleted;
        this.timeWorking = timeWorking;
        this.taskName = taskName;
        this.taskStatus = taskStatus;
        this.contentLength = contentLength;
        this.RangeManager.setRemaining((ArrayList<Range>) RangeProgression[0]);
        this.RangeManager.setCompleted((ArrayList<Range>) RangeProgression[1]);
    }

    public void setSourceURL(URL source) {
        this.source = source;
        this.taskStatus = Status.CONNECTING;
    }

    public void addOutputGUI(MainWindow mainWindow) {
        this.table = mainWindow.getTableTasks();
        this.mainWindow = mainWindow;
    }

    public MainWindow getMainWindow() {
        return mainWindow;
    }

    public void setTimeCreated(long timeCreated) {
        this.timeCreated = timeCreated;
        this.lastTimeUpdate = System.nanoTime();
    }

    public long getTimeCreated() {
        return this.timeCreated;
    }

    public void setTimeCompleted(long timeCompleted) {
        this.timeCompleted = timeCompleted;
    }

    public long getTimeCompleted() {
        long output = 0;
        if (timeCompleted == timeCreated) {
            output = System.currentTimeMillis()
                    + format.sTOms(getRemainingTime());
        } else {
            output = this.timeCompleted;
        }
        return output;
    }

    public void setLastTimeUpdate(long lastTimeUpdate) {
        this.lastTimeUpdate = lastTimeUpdate;
    }

    public long getLastTimeUpdate() {
        return lastTimeUpdate;
    }

    public void setTimeWorking(long timeWorking) {
        this.timeWorking = timeWorking;
    }

    public long getTimeWorking() {
        return timeWorking;
    }

    public Object[] saveHTTPTask() {
        Object[] data = new Object[3];
        ArrayList<String> names = new ArrayList<String>();
        ArrayList<String> values = new ArrayList<String>();

        names.add("URL");
        values.add(source.toExternalForm());

        names.add("FileDestination");
        values.add(destination.getAbsolutePath());

        names.add("NumWorkers");
        values.add(numWorkers + "");

        names.add("TaskName");
        values.add(taskName);

        names.add("TaskStatus");
        values.add(taskStatus.toString());

        names.add("MaxBufferSize");
        values.add(maxBufferSize + "");

        names.add("ContentLength");
        values.add(contentLength + "");

        names.add("TimeCreated");
        values.add(timeCreated + "");

        names.add("TimeCompleted");
        values.add(timeCompleted + "");

        names.add("TimeWorking");
        values.add(timeWorking + "");

        data[0] = names;
        data[1] = values;

        //if (!taskStatus.equals(Status.DONE)) {
        data[2] = saveTaskProgression();
        //}

        return data;
    }

    public DefaultTableModel getModel() {
        return (DefaultTableModel) table.getModel();
    }

    public void run() {
        // get all the info we can on the file
        if (taskStatus == Status.CONNECTING || taskStatus == Status.DOWNLOADING) {
            try {
                fetchInfo();
            } catch (IOException ex) {
                Logger.getLogger(HTTPTaskManager.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
        // create the file
        try {
            createFile();
        } catch (IOException ex) {
            Logger.getLogger(HTTPTaskManager.class.getName()).log(Level.SEVERE, null, ex);
        }

        // start stats updates
        timerTask = new UpdateTaskProgress(this);
        timer_main.scheduleAtFixedRate(timerTask, 100, 995);

        // now create the workers and start the download!
        setLastTimeUpdate(System.nanoTime());
        if (taskStatus == Status.CONNECTING) {
            createWorkers();
        }
    }

    private void fetchInfo() throws IOException {
        // setting up the connection
        HttpURLConnection connection = (HttpURLConnection) source.openConnection();

        // ask total range of the file
        connection.setRequestProperty("Range",
                "bytes=" + 0 + "-");

        // connect to the server
        connection.connect();

        // server response code handling
        if (connection.getResponseCode() / 100 != 2) {
            System.out.println("Server's response code produced an error!");
        }

        // get content length
        //contentLength = connection.getContentLength();
        contentLength = Long.parseLong(connection.getHeaderField("content-length"));
        if (contentLength < 1) {
            System.out.println("Content legnth cannot be negative!");
        }

        // get file name
        destination = new File(fileTools.formatPath(destination.getAbsolutePath() + "\\" + getFileName(source)));

        // now disconnect
        // -!!!- this might cause some problems with servers that only allow one single connection
        connection.disconnect();
    }

    private void createFile() throws IOException {
        System.out.println(destination.getAbsolutePath());
        boolean created = destination.createNewFile();
        raf = new RandomAccessFile(destination, "rw");
        if (created) {
            raf.setLength(contentLength);
        }
        raf.close();
    }

    private void createWorkers() {
        taskStatus = Status.DOWNLOADING;

        arWorkers = new DownloadWorker[numWorkers];
        for (int i = 0; i < arWorkers.length; i++) {
            //create new range
            Range range = new Range(i * contentLength / arWorkers.length,
                    (i + 1) * contentLength / arWorkers.length);
            //add range to rangemanager
            RangeManager.addRemainingRange(range);
            //create new worker
            arWorkers[i] = new DownloadWorker(source, lastWorkerID++,
                    range,
                    maxBufferSize, destination);
            //start new worker
            arWorkers[i].start();
            //set range as working
            RangeManager.addWorkingRange(range);
        }
    }

    public void stoppedWorkerHandler() {
        for (int i = 0; i < arWorkers.length; i++) {
            switch (arWorkers[i].getStatus()) {
                case ERROR:
                case STOPPED:
                    RangeManager.addCompletedRange(arWorkers[i].getRangeCompleted());
                    RangeManager.addRemainingRange(arWorkers[i].getRangeRemaining());
                    break;
                case DONE:
                    RangeManager.addCompletedRange(arWorkers[i].getRangeCompleted());
                    if (taskStatus.equals(Status.DOWNLOADING)) {
                        if (RangeManager.isRangeRemaining()) {
                            arWorkers[i].setNewRange(RangeManager.getFirstRemainingRange());
                        }
                    }
                    break;

            }
        }
    }

    private Object[] saveTaskProgression() {
        RangeManager.isRangeRemaining();
        ArrayList<Range> remaining = new ArrayList<Range>();
        ArrayList<Range> completed = new ArrayList<Range>();

        for (int i = 0; i < arWorkers.length; i++) {
            if (arWorkers[i].getStatus() != Status.DONE
                    || arWorkers[i].getStatus() != Status.AVAILABLE) {
                try {
                    remaining.add(arWorkers[i].getRangeRemaining());
                    completed.add(arWorkers[i].getRangeCompleted());
                } catch (Exception e) {
                }
            }
        }

        for (int i = 0; i < RangeManager.getRemainingRanges().size(); i++) {
            remaining.add(RangeManager.getRemainingRanges().get(i));
        }

        for (int i = 0; i < RangeManager.getCompletedRanges().size(); i++) {
            completed.add(RangeManager.getCompletedRanges().get(i));
        }

        Object[] object = new Object[2];
        object[0] = remaining;
        object[1] = completed;
        RangeManager.isRangeRemaining();

        return object;
    }

    public RangeManager getRangeManager() {
        return this.RangeManager;
    }

    public void updateUIOnce() {
        timer_temp.purge();
        timer_temp.schedule(new UpdateTaskProgress(this), 1);
    }

    private static String getFileName(URL url) {
        String chaine = url.getFile();
        return chaine.substring(chaine.lastIndexOf("/") + 1);
    }

    public String getSourceURL() {
        return source.toExternalForm();
    }

    public String getFileName() {
        String temp = source.getFile();
        if (temp.contains("/")) {
            temp = temp.substring(temp.lastIndexOf("/") + 1);
        }
        return temp;
    }

    public String getTaskName() {
        return taskName;
    }

    public void setTaskName(String taskname) {
        this.taskName = taskname;
    }

    public int getNumActiveWorkers() {
        return numActiveWorkers;
    }

    public int getMaxNumWorkers() {
        return numWorkers;
    }

    public int getNumWorkers() {
        return arWorkers.length;
    }

    public void setMaxNumWorkers(int numWorkers) {
        DownloadWorker temp[] = new DownloadWorker[numWorkers];
        if (this.numWorkers < numWorkers) {
            System.arraycopy(arWorkers, 0, temp, 0, arWorkers.length);
            //fill what remains of the array with new workers
            for (int i = arWorkers.length; i < temp.length; i++) {
                //add workers
                //search for slowest worker
                DownloadWorker slowest = arWorkers[0];
                for (int j = 1; j < arWorkers.length; j++) {
                    if (slowest.getRemainingSize() < arWorkers[i].getRemainingSize()) {
                        slowest = arWorkers[j];
                    }
                }

                /*
                 * //split remaining size in two long endPos =
                 * slowest.getEndPos(); long splitPos =
                 * slowest.splitRemaining();
                 */

                //create new range
                Range range = new Range(slowest.getEndPos(), slowest.splitRemaining());

                //create new worker
                temp[i] = new DownloadWorker(source, lastWorkerID,
                        range, maxBufferSize, destination);
            }
        } else if (this.numWorkers > numWorkers) {
            //delete workers
            DownloadWorker arConn[] = new DownloadWorker[this.numWorkers];
            DownloadWorker arDown[] = new DownloadWorker[this.numWorkers];
            DownloadWorker arStop[] = new DownloadWorker[this.numWorkers];
            int numConn = 0;
            int numDown = 0;
            int numStop = 0;

            //sort workers by status
            for (int i = 0; i < this.numWorkers; i++) {
                switch (arWorkers[i].getStatus()) {
                    case CONNECTING:
                        arConn[i] = arWorkers[numConn++];
                        break;
                    case DOWNLOADING:
                        arDown[i] = arWorkers[numDown++];
                        break;
                    default:
                        arStop[i] = arWorkers[numStop++];
                }
            }

            //move the workers we want back into the array
            //compute how many of each to move
            int downToMove = Math.min(numDown, numWorkers);
            int connToMove = Math.min(numConn, numWorkers - downToMove);
            int stopToMove = Math.min(numStop, numWorkers - downToMove - connToMove);

            //actually move them
            System.arraycopy(arDown, 0, temp, 0, downToMove);
            System.arraycopy(arConn, 0, temp, downToMove, connToMove);
            System.arraycopy(arStop, 0, temp, downToMove + stopToMove, stopToMove);
        }
        this.arWorkers = temp;
        this.numWorkers = numWorkers;
    }

    public int calcNumActiveWorkers() {
        int temp = 0;
        try {
            for (int i = 0; i < arWorkers.length; i++) {
                if (arWorkers[i].getStatus() == Status.DOWNLOADING
                        || arWorkers[i].getStatus() == Status.CONNECTING) {
                    temp++;
                } else if (arWorkers[i].getStatus() == Status.DONE) {
                    RangeManager.addCompletedRange(arWorkers[i].getFullRange());
                    arWorkers[i].setRangeDone();
                }
            }
        } catch (NullPointerException ex) {
            System.out.println("Worker array not built yet!");
            arWorkers = new DownloadWorker[temp];
        }
        this.numActiveWorkers = temp;
        return temp;
    }

    public void updateAvgSpeed() {
        lastAvgSpeed = 0;
        for (int i = 0; i < arWorkers.length; i++) {
            lastAvgSpeed += arWorkers[i].getSessionAvgSpeed();
        }
    }

    public double getTotalAvgSpeed() {
        return lastDownloadedTotal / format.nsTOs(timeWorking);
    }

    public double getAvgSpeed() {
        return lastAvgSpeed;
    }

    public void updateDownloadedSession() {
        lastDownloadedSession = 0;
        for (int i = 0; i < arWorkers.length; i++) {
            lastDownloadedSession += arWorkers[i].getSessionDownloaded();
        }
    }

    public long getDownloadedSession() {
        return lastDownloadedSession;
    }

    public void updateDownloadedTotal() {
        lastDownloadedTotal = 0;
        //add working ranges
        /*
        for (int i = 0; i < arWorkers.length; i++) {
            lastDownloadedTotal += arWorkers[i].getSessionDownloaded();
        }*/
        lastDownloadedTotal += RangeManager.getTotalSizeDownloaded();
        //add completed ranges
        //lastDownloadedTotal += RangeManager.getCompletedSize();
    }

    public long getDownloadedTotal() {
        return lastDownloadedTotal;
    }

    public void updateInstSpeed() {
        lastInstSpeed = 0;
        for (int i = 0; i < arWorkers.length; i++) {
            lastInstSpeed += arWorkers[i].getInstSpeed();
        }
    }

    public double getInstSpeed() {
        return lastInstSpeed;
    }

    public long getRemainingSize() {
        long remaining = contentLength - lastDownloadedTotal;
        if (remaining < 0) {
            remaining = 0;
        }
        return remaining;
    }

    public double getRemainingTime() {
        double d = 0;
        if (getRemainingSize() > 0) {
            return getRemainingSize() / lastInstSpeed;
        } else {
            return d;
        }
    }

    public void setTaskStatus(Status taskStatus) {
        this.taskStatus = taskStatus;
    }

    public Status getTaskStatus() {
        return taskStatus;
    }

    public double getTotalSize() {
        return contentLength - 1;
    }

    public double getProgress() {
        double progress = lastDownloadedTotal / getTotalSize() * 100;
        if (progress > 100) {
            progress = 100;
        }
        return progress;
    }

    public void stopTimerTask() {
        timer_main.cancel();
    }

    public void stopHTTPTask() {
        stopTimerTask();
        if (!arWorkers.equals(null)) {
            for (int i = 0; i < arWorkers.length; i++) {
                arWorkers[i].stopWorker();
            }
        }
    }
}

class UpdateTaskProgress extends TimerTask {

    private HTTPTaskManager manager;
    AppDataManager ADM = new AppDataManager();
    private FormatOutput format = new FormatOutput();
    private int row = 0;

    public UpdateTaskProgress(HTTPTaskManager manager) {
        this.manager = manager;
    }

    @Override
    public void run() {
        //log output
        //logOutput();

        //get current time
        long timeCurrent = System.nanoTime();

        //update necessary data
        findTableRow();
        manager.calcNumActiveWorkers();
        manager.updateInstSpeed();
        manager.updateAvgSpeed();
        manager.updateDownloadedSession();
        manager.updateDownloadedTotal();


        //stop download if no worker is active
        if (manager.getNumActiveWorkers() <= 0) {
            if (manager.getRemainingSize() <= 0) {
                System.out.println("task completed!");
                if (manager.getTimeCompleted() == manager.getTimeCreated()) {
                    manager.setTimeCompleted(timeCurrent);
                }
                manager.setTaskStatus(Status.DONE);
                manager.stopTimerTask();
                updateGUI_done();
                ADM.saveHTTPTask(manager.getTaskName(), manager.saveHTTPTask());
            } else if (manager.getTaskStatus() == Status.DONE) {
                //task already flagged as completed
                completeGUIUpdate();
            } else if (manager.getTaskStatus() == Status.STOPPED) {
                //task already stopped
            } else {
                System.out.println("ERROR! task stopped");
                System.out.println(manager.getNumActiveWorkers());
                System.out.println(manager.getRemainingSize());
                System.out.println(manager.getDownloadedTotal());
                manager.setTaskStatus(Status.ERROR);
                manager.stopTimerTask();
            }
        } else {
            //System.out.println("regular data refresh");
            //update working time
            manager.setTimeWorking(manager.getTimeWorking()
                    + (timeCurrent - manager.getLastTimeUpdate()));
            manager.setLastTimeUpdate(timeCurrent);

            //update GUI
            completeGUIUpdate();
        }

        //handle workers that are not working
        if (manager.getNumActiveWorkers() != manager.getNumWorkers()) {
            manager.stoppedWorkerHandler();
        }
    }

    private void completeGUIUpdate() {
        findTableRow();
        updateTableUI();
        updateSummaryUI();
    }

    private void updateSummaryUI() {
        if (row == manager.getMainWindow().getLastTaskSelected()) {
            //send to mainwindow for writing
            manager.getMainWindow().updateSummaryPanel(getSummaryDataList());
        }
    }

    private Object[] getSummaryDataList() {
        Object[] list = new Object[15];
        int i = 0;
        //fill with data
        list[i++] = (int) manager.getProgress();
        list[i++] = manager.getNumActiveWorkers() + "";
        list[i++] = format.percentage(manager.getProgress());
        list[i++] = format.size(manager.getDownloadedTotal());
        list[i++] = format.size(manager.getRemainingSize());
        list[i++] = format.size(manager.getTotalSize());
        list[i++] = format.speed(manager.getTotalAvgSpeed());
        list[i++] = format.speed(manager.getInstSpeed());
        list[i++] = "No upload";
        list[i++] = "No upload";
        list[i++] = manager.getTaskStatus().toString();
        list[i++] = format.date(manager.getTimeCreated());
        list[i++] = format.date(manager.getTimeCompleted());
        list[i++] = format.time(format.nsTOs(manager.getTimeWorking()));
        list[i++] = format.time(manager.getRemainingTime());
        //System.out.println(manager.getTimeCompleted());
        //System.out.println(manager.getTimeCreated());

        return list;
    }

    private void updateTableUI() {
        //write in table
        manager.getModel().setValueAt(manager.getTaskName(), row, 1);
        manager.getModel().setValueAt(format.size(manager.getTotalSize()), row, 1);
        manager.getModel().setValueAt(format.percentage(manager.getProgress()), row, 2);
        manager.getModel().setValueAt(format.time(manager.getRemainingTime()), row, 3);
        manager.getModel().setValueAt(format.speed(manager.getInstSpeed()), row, 4);
        manager.getModel().setValueAt(format.size(manager.getRemainingSize()), row, 6);
    }

    private void updateGUI_done() {
        //System.out.println("UpdateGUI_done");

        //update necessary data
        manager.updateInstSpeed();

        //regular screen refresh
        updateTableUI();
        updateSummaryUI();

        //specific refresh
        manager.getModel().setValueAt("", row, 3);
        manager.getModel().setValueAt("", row, 4);
        manager.getModel().setValueAt("", row, 6);
    }

    private void findTableRow() {
        row = manager.getMainWindow().getTableTaskManager().getTaskRow(manager.getTaskName());
    }
}
